package com.core.gwt.place.client;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.google.gwt.core.client.GWT;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceHistoryMapper;
import com.core.gwt.place.impl.Encoder;

/**
 * An alternative implementation of {@link PlaceHistoryMapper}. It replaces the
 * built-in GWT implementation and allows for a more modular approach to
 * assembling the application's place inventory.<br>
 * <br>
 * To use this class your place implementations must extend
 * {@link PlaceWithType}. Add the {@link PlaceType} objects of all your
 * application's places to the instance of this class used by your application.
 * For instance:
 * 
 * <pre>
 * public class AppPlaces {
 *     public static Collection{@literal<PlaceType>} get() {
 *         ArrayList{@literal<PlaceType>} places = new ArrayList{@literal<PlaceType>}();
 *         
 *         places.add(ThisPlace.TYPE);
 *         places.add(ThatPlace.TYPE);
 *         places.add(OtherPlace.TYPE);
 *         
 *         places.addAll(SomeOtherModulesPlaces.get());
 *         
 *         return places;
 *     }
 * }
 * ...
 * public class PlaceHistoryMapperProvider {
 *     public PlaceHistoryMapper get() {
 *         PlaceHistoryMapperUsingPlaceType mapper = new PlaceHistoryMapperUsingPlaceType();
 *         mapper.addAll(AppPlaces.get());
 *         return mapper;
 *     }
 * }
 * </pre>
 * 
 * Do not use {@link GWT#create} to create an instance of this class since that
 * will trigger the generation of {@link PlaceHistoryMapper} and will cause a
 * deferred binding error.
 * 

 */
public class PlaceHistoryMapperUsingPlaceType implements PlaceHistoryMapper {
	private static final Character STATE_DELIMITER = '?';
	private static String[][] ENCODING = { { "_", "__" }, { "?", "_q" },
			{ " ", "_s" }, { "\t", "_t" }, { "\r", "_r" }, { "\n", "_n" } };

	private final Map<String, PlaceType> prefixToTypeMap;

	public PlaceHistoryMapperUsingPlaceType() {
		prefixToTypeMap = new HashMap<String, PlaceType>();
	}

	/**
	 * Adds a place type to the history mapper.
	 * 
	 * @param placeType
	 *            a {@link PlaceType} object
	 */
	public void add(final PlaceType placeType) {
		assert (placeType != null);
		assert (placeType.getPrefix() != null) : "No place prefix?";
		assert (placeType.getPrefix().length() > 0) : "No place prefix?";
		assert (!prefixToTypeMap.containsKey(encode(placeType.getPrefix()))) : "Duplicate place prefix?";

		prefixToTypeMap.put(encode(placeType.getPrefix()), placeType);
	}

	/**
	 * Adds a collection of place types to the history mapper.
	 * 
	 * @param placeTypes
	 *            a {@link Collection} of {@link PlaceType} objects
	 */
	public void addAll(final Collection<PlaceType> placeTypes) {
		for (final PlaceType placeType : placeTypes) {
			add(placeType);
		}
	}

	/**
	 * Encodes a string, replacing all illegal characters with escaped
	 * representations.
	 */
	private String encode(final String value) {
		return Encoder.encode(value, ENCODING);
	}

	@Override
	public Place getPlace(final String token) {
		Place place = null;
		if (token != null) {
			final String splitRegex = "\\" + STATE_DELIMITER;
			final String[] parts = token.split(splitRegex, 2);
			final String prefix = parts.length > 0 ? parts[0] : null;
			final String state = parts.length > 1 ? parts[1] : null;
			final PlaceType placeType = prefixToTypeMap.get(prefix);
			if (placeType != null) {
				place = placeType.getPlace(state);
			}
		}
		return place;
	}

	@Override
	public String getToken(final Place place) {
		final StringBuilder token = new StringBuilder();
		if (place instanceof PlaceWithType) {
			final PlaceType placeType = ((PlaceWithType) place).getType();
			if (placeType != null) {
				final String prefix = placeType.getPrefix();
				if ((prefix != null ? prefix.length() : 0) > 0) {
					token.append(encode(prefix));
					final String state = ((PlaceWithType) place).toToken();
					if ((state != null ? state.length() : 0) > 0) {
						token.append(STATE_DELIMITER);
						token.append(state);
					}
				}
			}
		}
		return token.toString();
	}
}
